// Copyright (c) Facebook, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::collections::HashMap;
use std::collections::HashSet;

use cursive::utils::markup::StyledString;
use itertools::Itertools;
use model::Queriable;
use model::SingleProcessModel;

use crate::process_view::ProcessState;
use crate::render::ViewItem;
use crate::stats_view::ColumnTitles;
use crate::stats_view::StateCommon;

/// Renders corresponding Fields From ProcessModel.
type ProcessViewItem = ViewItem<model::SingleProcessModelFieldId>;

/// A collection of ProcessViewItem.
#[derive(Clone)]
pub struct ProcessTab {
    pub view_items: Vec<ProcessViewItem>,
}

// Defines how to iterate through the process stats and generate get_rows for ViewBridge
impl ProcessTab {
    fn new(view_items: Vec<ProcessViewItem>) -> Self {
        Self { view_items }
    }

    fn get_process_field_line(
        &self,
        model: &SingleProcessModel,
        depth: usize,
        offset: Option<usize>,
    ) -> StyledString {
        let mut line = StyledString::new();
        line.append(default_tabs::COMM_VIEW_ITEM.render_indented_depth(model, depth));
        line.append_plain(" ");

        for item in std::iter::once(&*default_tabs::CGROUP_VIEW_ITEM)
            .chain(self.view_items.iter())
            .skip(offset.unwrap_or(0))
        {
            line.append(item.render(model));
            line.append_plain(" ");
        }

        line
    }

    pub fn get_titles(&self) -> ColumnTitles {
        ColumnTitles {
            titles: std::iter::once(&*default_tabs::COMM_VIEW_ITEM)
                .chain(std::iter::once(&*default_tabs::CGROUP_VIEW_ITEM))
                .chain(self.view_items.iter())
                .map(|item| item.config.render_title())
                .collect(),
            pinned_titles: 1,
        }
    }

    fn render_process_tree(
        &self,
        tree: &HashMap<i32, Vec<&SingleProcessModel>>,
        p: &SingleProcessModel,
        depth: usize,
        offset: Option<usize>,
        visited: &mut HashSet<i32>,
        output: &mut Vec<(StyledString, i32)>,
    ) {
        let pid = p.pid.unwrap_or(0);
        // Show each process only once
        if visited.insert(pid) {
            output.push((self.get_process_field_line(p, depth, offset), pid));
            for c in tree.get(&pid).unwrap_or(&Vec::new()) {
                self.render_process_tree(tree, c, depth + 1, offset, visited, output);
            }
        }
    }

    pub fn get_rows(
        &self,
        state: &ProcessState,
        offset: Option<usize>,
    ) -> Vec<(StyledString, i32)> {
        let unknown = "?".to_string();
        let unknown_pid: i32 = -1;
        let process_model = state.get_model();
        let mut processes: Vec<&SingleProcessModel> = process_model.processes.values().collect();

        if let Some(sort_order) = state.sort_order.as_ref() {
            model::sort_queriables(&mut processes, sort_order, state.reverse);
        }

        processes = processes
            .into_iter()
            .filter(|spm| {
                // If we're in zoomed pids mode, only show processes belonging
                // to set of pids
                if let Some(f) = &state.pids_filter {
                    f.contains(&spm.pid.unwrap_or(unknown_pid))
                } else {
                    true
                }
            })
            .filter(|spm| {
                // If we're in zoomed cgroup mode, only show processes belonging to
                // our zoomed cgroup
                if let Some(f) = &state.cgroup_filter {
                    spm.cgroup.as_ref().unwrap_or(&unknown).starts_with(f)
                } else {
                    true
                }
            })
            .filter(|spm| {
                // If we're filtering by selected field, only show processes who pass the filter
                if let Some((field_id, filter)) = &state.filter_info {
                    match spm.query(field_id) {
                        None => true,
                        Some(value) => value.to_string().contains(filter),
                    }
                } else {
                    true
                }
            })
            .collect();

        if state.tree_view {
            let mut tree: HashMap<i32, Vec<&SingleProcessModel>> = HashMap::new();
            let mut pids: HashSet<i32> = HashSet::new();

            for p in &processes {
                if let Some(ppid) = p.ppid {
                    tree.entry(ppid).or_default().push(p);
                }

                if let Some(pid) = p.pid {
                    pids.insert(pid);
                }
            }

            let mut output = Vec::new();
            let mut visited = HashSet::new();
            for p in processes {
                let ppid = p.ppid.unwrap_or(0);
                // it's pid 1 or parent is not visible
                if ppid == 0 || !pids.contains(&ppid) {
                    self.render_process_tree(&tree, p, 0, offset, &mut visited, &mut output);
                }
            }
            output
        } else {
            processes
                .into_iter()
                .cloned()
                // Abuse batching() to conditionally fold iter
                .batching(|it| {
                    if state.fold {
                        it.next().map(|first| {
                            it.fold(first.clone(), |acc, spm| {
                                SingleProcessModel::fold(&acc, &spm)
                            })
                        })
                    } else {
                        it.next()
                    }
                })
                .map(|spm| {
                    (
                        self.get_process_field_line(&spm, 0, offset),
                        spm.pid.unwrap_or(0),
                    )
                })
                .collect()
        }
    }
}

pub mod default_tabs {
    use common::util::get_prefix;
    use model::ProcessCpuModelFieldId::NumThreads;
    use model::ProcessCpuModelFieldId::Processor;
    use model::ProcessCpuModelFieldId::SystemPct;
    use model::ProcessCpuModelFieldId::UsagePct;
    use model::ProcessCpuModelFieldId::UserPct;
    use model::ProcessIoModelFieldId::RbytesPerSec;
    use model::ProcessIoModelFieldId::RwbytesPerSec;
    use model::ProcessIoModelFieldId::WbytesPerSec;
    use model::ProcessMemoryModelFieldId::Anon;
    use model::ProcessMemoryModelFieldId::File;
    use model::ProcessMemoryModelFieldId::HugeTlb;
    use model::ProcessMemoryModelFieldId::Lock;
    use model::ProcessMemoryModelFieldId::MajorfaultsPerSec;
    use model::ProcessMemoryModelFieldId::MinorfaultsPerSec;
    use model::ProcessMemoryModelFieldId::Pin;
    use model::ProcessMemoryModelFieldId::Pte;
    use model::ProcessMemoryModelFieldId::RssBytes;
    use model::ProcessMemoryModelFieldId::Shmem;
    use model::ProcessMemoryModelFieldId::Swap;
    use model::ProcessMemoryModelFieldId::VmSize;
    use model::SingleProcessModelFieldId::Cgroup;
    use model::SingleProcessModelFieldId::Cmdline;
    use model::SingleProcessModelFieldId::Comm;
    use model::SingleProcessModelFieldId::Cpu;
    use model::SingleProcessModelFieldId::Io;
    use model::SingleProcessModelFieldId::Mem;
    use model::SingleProcessModelFieldId::NsTgid;
    use model::SingleProcessModelFieldId::Pid;
    use model::SingleProcessModelFieldId::Ppid;
    use model::SingleProcessModelFieldId::Stack;
    use model::SingleProcessModelFieldId::State;
    use model::SingleProcessModelFieldId::UptimeSecs;
    use once_cell::sync::Lazy;

    use super::*;

    pub static COMM_VIEW_ITEM: Lazy<ProcessViewItem> = Lazy::new(|| {
        use base_render::RenderConfigBuilder;
        ViewItem::from_default(Comm)
            .update(RenderConfigBuilder::new().indented_prefix(get_prefix(false)))
    });
    pub static CGROUP_VIEW_ITEM: Lazy<ProcessViewItem> =
        Lazy::new(|| ViewItem::from_default(Cgroup));

    pub static PROCESS_GENERAL_TAB: Lazy<ProcessTab> = Lazy::new(|| {
        ProcessTab::new(vec![
            ViewItem::from_default(Pid),
            ViewItem::from_default(Ppid),
            ViewItem::from_default(NsTgid),
            ViewItem::from_default(State),
            ViewItem::from_default(Cpu(UsagePct)),
            ViewItem::from_default(Cpu(UserPct)),
            ViewItem::from_default(Cpu(SystemPct)),
            ViewItem::from_default(Mem(RssBytes)),
            ViewItem::from_default(Mem(MinorfaultsPerSec)),
            ViewItem::from_default(Mem(MajorfaultsPerSec)),
            ViewItem::from_default(Io(RbytesPerSec)),
            ViewItem::from_default(Io(WbytesPerSec)),
            ViewItem::from_default(UptimeSecs),
            ViewItem::from_default(Cpu(NumThreads)),
            ViewItem::from_default(Io(RwbytesPerSec)),
            ViewItem::from_default(Cmdline),
            ViewItem::from_default(Stack),
        ])
    });

    pub static PROCESS_CPU_TAB: Lazy<ProcessTab> = Lazy::new(|| {
        ProcessTab::new(vec![
            ViewItem::from_default(Cpu(UserPct)),
            ViewItem::from_default(Cpu(SystemPct)),
            ViewItem::from_default(Cpu(NumThreads)),
            ViewItem::from_default(Cpu(UsagePct)),
            ViewItem::from_default(Cpu(Processor)),
        ])
    });

    pub static PROCESS_MEM_TAB: Lazy<ProcessTab> = Lazy::new(|| {
        ProcessTab::new(vec![
            ViewItem::from_default(Mem(RssBytes)),
            ViewItem::from_default(Mem(VmSize)),
            ViewItem::from_default(Mem(Swap)),
            ViewItem::from_default(Mem(Anon)),
            ViewItem::from_default(Mem(File)),
            ViewItem::from_default(Mem(Shmem)),
            ViewItem::from_default(Mem(Pte)),
            ViewItem::from_default(Mem(Lock)),
            ViewItem::from_default(Mem(Pin)),
            ViewItem::from_default(Mem(HugeTlb)),
            ViewItem::from_default(Mem(MinorfaultsPerSec)),
            ViewItem::from_default(Mem(MajorfaultsPerSec)),
        ])
    });

    pub static PROCESS_IO_TAB: Lazy<ProcessTab> = Lazy::new(|| {
        ProcessTab::new(vec![
            ViewItem::from_default(Io(RbytesPerSec)),
            ViewItem::from_default(Io(WbytesPerSec)),
            ViewItem::from_default(Io(RwbytesPerSec)),
        ])
    });
}
